<?php
namespace pmvc\annotation;

use pmvc\cache\CacheUtil;
use pmvc\cache\LocalCacheImpl;
use pmvc\cache\Cache;

use pmvc\util\ReflectionUtil;

use ReflectionClass;
use ReflectionMethod;
use ReflectionProperty;
use ReflectionFunction;
use Exception;
use InvalidArgumentException;

/**
 * Implementation of the {@link AnnotationReader} interface
 * that delegates to an underlying {@link AnnotationReader}
 * and caches all reads.
 *
 */
class CachedAnnotationReaderImpl
	implements AnnotationReader {

	private $cache;
	private $ttl = Cache::ONE_HOUR;
	private $annotationReader;

	/**
	 * Constructs the {@link CachedAnnotationReader} with the
	 * given underlying {@link AnnotationReader} and {@link Cache}.
	 * if a {@link Cache} is not specified than 
	 * 
	 * @param AnnotationReader $annotationReader
	 * @param Cache $cache
	 */
	public function __construct(AnnotationReader $annotationReader, Cache $cache=null) {
		$this->annotationReader = $annotationReader;
		$this->cache = $cache;
		if (!isset($this->cache)) {
			$this->cache = new LocalCacheImpl();
		}
	}

	/**
	 * Sets the {@link Cache} that this {@link AnnotationReader} uses
	 * to cache annotations.
	 * @param Cache $cache
	 */
	public function setCache(Cache $cache) {
		$this->cache = $cache;
	}

	/**
	 * Sets the TTL (time to live) for entries that this
	 * 
	 * @param int $ttl
	 */
	public function setTtl($ttl) {
		$this->ttl = intval($ttl);
	}

	/**
	 * {@inheritDoc}
	 */
	public function readClassAnnotations($class, Array $types=Array()) {
		if (is_string($class)) {
			$class = new ReflectionClass($class);
		}
		$annotationReader = $this->annotationReader;
		return $this->getFromCache($class, "class", $types,
			function()use($annotationReader, $class, $types) { 
				return $annotationReader->readClassAnnotations($class, $types);
			}
		);
	}

	/**
	 * {@inheritDoc}
	 */
	public function readPropertyAnnotations($property, Array $types=Array()) {
		if (is_array($property)) {
			$class = new ReflectionClass($property[0]);
			$property = $class->getProperty($property[1]);
		}
		$annotationReader = $this->annotationReader;
		return $this->getFromCache($property, "property", $types,
			function()use($annotationReader, $property, $types) { 
				return $annotationReader->readPropertyAnnotations($property, $types);
			}
		);
	}

	/**
	 * {@inheritDoc}
	 */
	public function readMethodAnnotations($method, Array $types=Array()) {
		if (is_array($method)) {
			$class = new ReflectionClass($method[0]);
			$method = $class->getMethod($method[1]);
		}
		$annotationReader = $this->annotationReader;
		return $this->getFromCache($method, "method", $types,
			function()use($annotationReader, $method, $types) { 
				return $annotationReader->readMethodAnnotations($method, $types);
			}
		);
	}

	/**
	 * {@inheritDoc}
	 */
	public function readFunctionAnnotations($function, Array $types=Array()) {
		if (!ReflectionUtil::isInstance($function, 'ReflectionFunction')) {
			$function = new ReflectionFunction($function);
		}
		$annotationReader = $this->annotationReader;
		return $this->getFromCache($function, "function", $types,
			function()use($annotationReader, $function, $types) { 
				return $annotationReader->readFunctionAnnotations($function, $types);
			}
		);
	}
	
	/**
	 * Returns annotations from the {@link Cache} if it
	 * exists, otherwise it calls {@code $fun} and caches
	 * it for subsequent reads.
	 * @param string $key
	 * @param callable $fun
	 * @return the value
	 */
	private function getFromCache($key, $prefix, Array $types=Array(), $fun) {
		if (!isset($key)) {
			throw new InvalidArgumentException("Invalid key ".$key);
		}

		// generate cache key
		$fileName = null;
		if ($key instanceof \ReflectionClass) {
			$cacheKey = $key->getName();
			$fileName = $key->getFileName();
			
		} else if ($key instanceof \ReflectionMethod) {
			$cacheKey = $key->getDeclaringClass()->getName()."::".$key->getName();
			$fileName = $key->getFileName();
			
		} else if ($key instanceof \ReflectionProperty) {
			$cacheKey = $key->getDeclaringClass()->getName()."::".$key->getName();
			$fileName = $key->getDeclaringClass()->getFileName();
			
		} else if ($key instanceof \ReflectionFunction) {
			$cacheKey = $key->getName();
			$fileName = $key>getFileName();
			
		} else if (!is_string($key)) {
			throw new Exception("Invalid key ".get_class($key));
		}
		
		$cacheKey = __CLASS__."__".$prefix."__".$cacheKey."__".join("_", $types);

		// the value to return
		$val = CacheUtil::statGet($fileName, $this->cache, $cacheKey, null);

		// update the value
		if (!isset($val)) {
			$val = $fun();
			CacheUtil::statSet($fileName, $this->cache, $cacheKey, $val);
		}

		// check the cache for the value
		return $val;
	}

}

?>